/*
 * Reksio - Memory Map Editor
 * Copyright (C) 2023 CERN
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In applying this licence, CERN does not waive the privileges and immunities
 * granted to it by virtue of its status as an Intergovernmental Organization or
 * submit itself to any jurisdiction.
 */

#include "validator.h"
#include <iostream>
#include <stdexcept>
#include "pythonvalidator.h"
#include "memorynode.h"

bool SchemaValidator::validate(const YAML::Node &schema, const YAML::Node &node, const bool strict_validation)
{
    schema_path.clear();
    node_path.clear();
    node_full_name.clear();
    return _validate(schema, node, strict_validation);
}

std::vector<std::unique_ptr<INodeRuleValidator>> SchemaValidator::validatorFactory(const YAML::Node &schema_node, const std::string& node_name)
{
    std::vector<std::unique_ptr<INodeRuleValidator>> validators;
    if(!schema_node.IsMap()) // we expect set of rules
    {
        return validators;
    }
    for(const auto& kv: schema_node)
    {
        const std::string& key = kv.first.Scalar();

        if(key == "required")
            validators.emplace_back(std::make_unique<NodeRequiredValidator>(schema_node, node_name));
        else if(key == "type")
            validators.emplace_back(std::make_unique<NodeTypeValidator>(schema_node, node_name));
        else if(key == "enum")
            validators.emplace_back(std::make_unique<NodeEnumValidator>(schema_node, node_name));
        else if(key == "regex")
            validators.emplace_back(std::make_unique<NodeRegexValidator>(schema_node, node_name));
        else if(key == "range")
            validators.emplace_back(std::make_unique<NodeRangeValidator>(schema_node, node_name));
        else if(key == "py_valid")
            validators.emplace_back(std::make_unique<PythonValidator>(schema_node, node_name));
        else if(key == "py_enum")
            validators.emplace_back(std::make_unique<PyNodeEnumValidator>(schema_node, node_name));
    }
    return validators;
}

bool SchemaValidator::_validate(const YAML::Node &schema, const YAML::Node &node, const bool fromSequence, const bool strict_validation)
{
    // lets start with root node
    switch(node.Type())
    {
        case YAML::NodeType::Map:
        {
            return validateMapping(schema, node, fromSequence, strict_validation);
        }
        case YAML::NodeType::Sequence:
        {
            return validateSequence(schema, node, strict_validation);
        }
        case YAML::NodeType::Scalar:
        {
            return validateScalar(schema, node, strict_validation);
        }
        case YAML::NodeType::Null:
        {
            return true;
        }
        case YAML::NodeType::Undefined:
        {
                logError("Not valid node!");
                return false;
        }
    }
    return false;
}

bool SchemaValidator::validateMapping(const YAML::Node &schema, const YAML::Node &node, const bool fromSequence, const bool strict_validation)
{
    const std::string& type = schema["type"].Scalar(); // todo check
    // lets find out if it's correct according to the schema
    const YAML::Node& schema_mapping = schema["mapping"];
    schema_path.push_back("mapping");
    if(!schema_mapping || !schema_mapping.IsMap())
    {
        logError("Badly formed schema! Mapping expected");
        return false;
    }
    if (type != "map")
    {
        logError("Type should be 'map', instead of " + type);
        return false;
    }

    // save node name for more detailed error message
    const YAML::Node& node_name = node["name"];
    if (node_name)
    {
        const std::string& name = node_name.as<std::string>();
        node_full_name.push_back(name);
    }

    bool allValid = true;

    for(const auto& kv: node) // iterate over map items
    {
        node_path.push_back(kv.first.as<std::string>());
        // check if every node is in the schema
        if (const YAML::Node& node_schema = schema_mapping[kv.first.as<std::string>()])
        {
            schema_path.push_back(kv.first.as<std::string>());
            allValid = _validate(node_schema, kv.second, false, strict_validation) && allValid;
            schema_path.pop_back();
        }
        else
        {
            // node not in schema
            // check if we're coming from a sequence
            if(!fromSequence)
            {
                // brother from schema may validate it properly, so don't log error
                logError("Node " + kv.first.Scalar() + " not in schema!");
            }
            allValid = false;
        }
        node_path.pop_back();
    }
    if (strict_validation)
    {
        for(const auto& kv: schema_mapping) // iterate over schema
        {
            schema_path.push_back(kv.first.as<std::string>());
            // check if every node required by schema is there
            if(!node[kv.first.as<std::string>()])
            {
                // node not present, but it's in the schema
                // lets check if it's required one (but only this)
                if(const YAML::Node& required_node = kv.second["required"])
                {
                    if(required_node.as<bool>(false))
                    {
                        logError("node " + kv.first.Scalar() + " is required!");
                        allValid = false;
                    }
                }
                else
                {
                    // not required
                }
            }
            schema_path.pop_back();
        }
    }
    schema_path.pop_back();

    // extra check required - not all nodes contain the 'name' attrubute
    if (node_name && (!node_full_name.empty()))
        node_full_name.pop_back();

    return allValid;
}

bool SchemaValidator::validateSequence(const YAML::Node &schema, const YAML::Node &node, const bool strict_validation)
{
    const std::string& type = schema["type"].Scalar(); // todo check
    const YAML::Node& schema_sequence = schema["sequence"];
    schema_path.push_back("sequence");
    if(!schema_sequence || !schema_sequence.IsSequence())
    {
        logError("Badly formed schema! Sequence expected");
        return false;
    }
    if (type != "seq")
    {
        logError("Type should be 'seq', instead of " + type);
        return false;
    }

    size_t seq_no = 0;
    bool allValid = true;

    for(const auto& v_node: node) // here iterate over nodes
    {
        node_path.push_back(std::to_string(seq_no));
        // depending on matching rule;
        // any - Each list item must satisfy at least one subrules
        // all - Each list item must satisfy every subrule
        // *   - At least one list item must satisfy at least one subrule
        bool singleValid = false;
        size_t schema_no = 0;
        for(const auto& v_schema : schema_sequence)
        {
            schema_path.push_back(std::to_string(schema_no));
            singleValid = _validate(v_schema, v_node, true, strict_validation);
            ++schema_no;
            schema_path.pop_back();
            if(singleValid)
                break;
        }
        if(!singleValid)
        {
            std::ostringstream str;
            str << v_node;
            logError("Node: \n" + str.str() + "\n not in schema!");
        }
        allValid = singleValid && allValid;
        ++seq_no;
        node_path.pop_back();
    }
    schema_path.pop_back();
    return allValid;
}

bool SchemaValidator::validateScalar(const YAML::Node &schema, const YAML::Node &node, const bool strict_validation)
{
    // create rules for checking
    createValidators(schema);
    bool okay = true;
    if(strict_validation)
    {
        for (const auto& validator : validators[getPath(schema_path)])
        {
            const bool validation_result = validator->validate(node);
            okay = validation_result && okay;
            if (!validation_result)
                logError(validator->getMessage());
        }
    }
    return okay;
}

void SchemaValidator::createValidators(const YAML::Node &schema_node)
{
    // @TODO CREATE FACTORY FOR VALIDATORS TO REUSE IN YAML SCHEMA FOR QT
    // first check if a node is already processed
    if(validators.find(getPath(schema_path)) != validators.end())
    {
        return;
    }
    if(!schema_node.IsMap()) // we expect set of rules
    {
        return;
    }

    if(schema_path.empty())
        return;

    const std::string node_name = *(--schema_path.end());

    for(const auto& kv: schema_node)
    {
        const std::string key = kv.first.Scalar();
        if(key == "required")
            validators[getPath(schema_path)].emplace_back(std::make_unique<NodeRequiredValidator>(schema_node, node_name));
        else if(key == "type")
            validators[getPath(schema_path)].emplace_back(std::make_unique<NodeTypeValidator>(schema_node, node_name));
        else if(key == "enum")
            validators[getPath(schema_path)].emplace_back(std::make_unique<NodeEnumValidator>(schema_node, node_name));
        else if(key == "regex")
            validators[getPath(schema_path)].emplace_back(std::make_unique<NodeRegexValidator>(schema_node, node_name));
        else if(key == "range")
            validators[getPath(schema_path)].emplace_back(std::make_unique<NodeRangeValidator>(schema_node, node_name));
        else if(key == "py_valid")
            validators[getPath(schema_path)].emplace_back(std::make_unique<PythonValidator>(schema_node, node_name));
    }

}

std::string SchemaValidator::getPath(std::vector<std::string> &path)
{
    std::string res;
    for(const auto& str: path)
        res += str + "/";
    return res;
}

void SchemaValidator::logError(const std::string &err)
{
    std::string str_schema_path;
    for(const auto& str: schema_path)
        str_schema_path += str + "/";

    std::string str_node_path;
    for(const auto& str: node_path)
        str_node_path += str + "/";

    std::string str_node_full_name;
    for(const auto& str: node_full_name)
        str_node_full_name += str + "/";

    errors.push_back(
        "Node path: " + str_node_path + "\n"
        "Node full name: " + str_node_full_name + "\n"
        "Schema path: " + str_schema_path + "\n"
        "Error: " + err + "\n"
    );
}


NodeRequiredValidator::NodeRequiredValidator(const YAML::Node &schema_node, const std::string& node_name): INodeRuleValidator (node_name)
{
    if(const YAML::Node& required_node = schema_node["required"])
    {
        required = required_node.as<bool>();
    }
    else
        throw std::runtime_error("Required validator is wrong");
}

bool NodeRequiredValidator::validate(const YAML::Node &node) const
{
    return required ? node.IsDefined() : true;
}

bool NodeRequiredValidator::validate(const std::string &value) const
{
    return required ? !value.empty() : true;
}

std::string NodeRequiredValidator::getMessage() const
{
    return "Attribute " + node_name + " is required!";
}

bool NodeRequiredValidator::isRequired() const
{
    return required;
}

NodeTypeValidator::NodeTypeValidator(const YAML::Node &schema_node, const std::string& node_name): INodeRuleValidator (node_name)
{
    if(const YAML::Node& type_node = schema_node["type"])
    {
        target_type = type_node.Scalar();
    }
    else
        throw std::runtime_error("Type validator is wrong");
}

bool NodeTypeValidator::validate(const YAML::Node &node) const
{
    try
    {
        if(target_type == "int" || target_type == "hex")
            node.as<long long int>();
        else if(target_type == "bool")
            node.as<bool>();
        else if(target_type == "float")
            node.as<float>();
        else if(target_type == "str" || target_type == "long_str")
            node.as<std::string>();
        else if(target_type == "file")
            node.as<std::string>();
        else
            return false;
    }
    catch (YAML::BadConversion&)
    {
        return false;
    }
    return true;
}

bool NodeTypeValidator::validate(const std::string & val) const
{
	static_cast<void>(val); // suppress -Wunused-parameter
    return true; // cannot do more here.
}

std::string NodeTypeValidator::getMessage() const
{
    if(target_type == "file")
        return "File doesn't exist!";
    return node_name + "'s type is wrong! Expected: " + target_type;
}

const std::string &NodeTypeValidator::getTargetType() const
{
    return target_type;
}

NodeEnumValidator::NodeEnumValidator(const YAML::Node &schema_node, const std::string& node_name): INodeRuleValidator (node_name)
{
    const YAML::Node& enum_node = schema_node["enum"];
    if(enum_node && enum_node.IsSequence())
    {
        for(const auto& v: enum_node)
        {
            enum_values.emplace_back(v.Scalar());
        }
    }
    else
        throw std::runtime_error("Enum validator is wrong!");
}

bool NodeEnumValidator::validate(const std::string &value) const
{
    return std::find(enum_values.begin(), enum_values.end(), value) != enum_values.end();
}

std::string NodeEnumValidator::getMessage() const
{
    std::string msg = node_name + "'s value is wrong! Should be one of: [";
    for(const auto& v: enum_values)
        msg += "'" + v + "', ";
    msg += "]";
    return msg;
}

const std::vector<std::string> &NodeEnumValidator::getEnums() const
{
    return enum_values;
}

NodeRegexValidator::NodeRegexValidator(const YAML::Node &schema_node, const std::string& node_name): INodeRuleValidator (node_name)
{
    const YAML::Node& regex_node = schema_node["regex"];
    if(regex_node && regex_node.IsScalar())
    {
        regex_str = regex_node.Scalar();
        regex = regex_str;
    }
    else
        throw std::runtime_error("Regex validator is wrong!");
}

bool NodeRegexValidator::validate(const std::string &value) const
{
    return std::regex_match(value, regex);
}

std::string NodeRegexValidator::getMessage() const
{
    std::string msg = node_name + "'s value is wrong! It should match: '" + regex_str + "' pattern.";
    return msg;
}

std::string NodeRegexValidator::getRegexStr() const
{
    return regex_str;
}

NodeRangeValidator::NodeRangeValidator(const YAML::Node &schema_node, const std::string& node_name): INodeRuleValidator (node_name)
{
    const YAML::Node& range_node = schema_node["range"];
    if(range_node && range_node.IsMap())
    {
        try
        {
            min_ex_en = range_node["min-ex"];
            min_en = range_node["min"];
            max_ex_en = range_node["max-ex"];
            max_en = range_node["max"];

            if(min_ex_en)
                min = range_node["min-ex"].as<float>();
            if(min_en)
                min = range_node["min"].as<float>();
            if(max_ex_en)
                max = range_node["max-ex"].as<float>();
            if(max_en)
                max = range_node["max"].as<float>();

            // check if it makes sense
            if ((min_en || min_ex_en) && (max_en || max_ex_en))
            {
                if(min > max)
                    throw std::runtime_error("Range validator is wrong!");
            }
            if (!(min_en || min_ex_en || max_en || max_ex_en))
                throw std::runtime_error("Range validator is wrong!");

        }
        catch (YAML::BadConversion&)
        {
            throw std::runtime_error("Range validator is wrong!");
        }
    }
    else
        throw std::runtime_error("Range validator is wrong!");
}

bool NodeRangeValidator::validate(const std::string &value) const
{
    double val;
    try {
        val = std::stod(value);
    } catch (const std::invalid_argument&) {
        return false;
    }

    bool result = true;
    if(min_ex_en)
        result = val > min;
    else if(min_en)
        result = val >= min;
    if(max_ex_en)
        result = (val < max) && result;
    else if(max_en)
        result = (val <= max) && result;
    return result;
}

std::string NodeRangeValidator::getMessage() const
{
    std::string msg = node_name + "'s value is wrong! It should be ";
    if (min_en)
        msg += ">= " + std::to_string(min);
    if (min_ex_en)
        msg += "> " + std::to_string(min);

    if (max_ex_en || max_en)
        msg += " and ";

    if (max_en)
        msg += "<= " + std::to_string(max);
    if (max_ex_en)
        msg += "< " + std::to_string(max);

    return msg;
}

bool NodeRangeValidator::hasMin() const
{
    return min_en || min_ex_en;
}

bool NodeRangeValidator::hasMax() const
{
    return max_en || max_ex_en;
}

double NodeRangeValidator::getMin() const
{
    if(min_ex_en)
    {
        return min+1;
    }
    else if(min_en)
    {
        return min;
    }
    return std::numeric_limits<double>::min();
}

double NodeRangeValidator::getMax() const
{
    if(max_ex_en)
    {
        return max-1;
    }
    else if(max_en)
    {
        return max;
    }
    return std::numeric_limits<double>::max();
}

bool INodeRuleValidator::validate(const YAML::Node &node) const
{
    return validate(node.as<std::string>());
}

bool INodeRuleValidator::validate(const Attribute* attr) const
{
    return validate(attr->getValue());
}

const std::string &INodeRuleValidator::nodeName() const
{
    return node_name;
}
